﻿/*
ParallelPlatform Project - A web platform to distribute and share knowledge
Online Editor - An online tool to create and play animations
Copyright (C) 2011 Daniel Guerrero Martínez & Sergio Rodríguez Lumley

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package Classes{
	public class FileModule extends BasicFileModule{
		
		/*
			BasicFileModule methods
		This is a workaround the issue that ActionScript3 does not allow
		inheritance of static methods, but they can be used within this
		class.
		*/
		static public function encrypt(str:String):String{
			return BasicFileModule.encrypt(str);
		}
		
		static public function decrypt(str:String):String{
			return BasicFileModule.decrypt(str);
		}
		
		static public function compact(str:String):String{
			return BasicFileModule.compact(str);
		}
		
		static public function decompact(str:String):String{
			return BasicFileModule.decompact(str);
		}
		
		static public function stringToXML(str:String):XML{
			return BasicFileModule.stringToXML(str);
		}
		
		static public function xmlToString(xmlFile:XML):String{
			return BasicFileModule.xmlToString(xmlFile);
		}
		
		static public function getTitle(xmlFile:XML):String{
			return BasicFileModule.getTitle(xmlFile);
		}
		
		static public function getDescription(xmlFile:XML):String{
			return BasicFileModule.getDescription(xmlFile);
		}
		
		static public function getDelay(xmlFile:XML):Number{
			return BasicFileModule.getDelay(xmlFile);
		}
		
		static public function getSceneList(xmlFile:XML):XMLList{
			return BasicFileModule.getSceneList(xmlFile);
		}
		
		static public function getSceneActions(sceneElement:XML):XMLList{
			return BasicFileModule.getSceneActions(sceneElement);
		}
		
		static public function getSceneType(sceneElement:XML):String{
			return BasicFileModule.getSceneType(sceneElement);
		}
		
		static public function getAuthors(sceneElement:XML):Array{
			return BasicFileModule.getAuthors(sceneElement);
		}
		/*    End of the workaround     */
		
		
		/**
		  Completes the list of actions with the specified actions in the xml actionList
		  @param actions ActionList list of action objects to fill
		  @param actionList XMLList with the actions to insert, in XML format
		  @return Vector.<uint> index of actions clustered
		*/
		static public function completeEditorActions(actions:ActionList, actionList:XMLList):Vector.<uint>{
			var actIndex:Vector.<uint> = new Vector.<uint>();
			var index:uint = 0;
			var lastId:String = "x";
			// Changing factors!
			// We will take in count to what shapes we are adding
			// actions, the moment it changes we will add a water-mark
			// to specify to the actions list that the last cluster of actions
			// is closed and we open a new one
			for each(var actionElement:XML in actionList){
				if(lastId != String(actionElement.id.text()) ){
					lastId = String(actionElement.id.text());
					actIndex.push(index);
				}
				index += xmlInsertAction(actionElement, actions);
			}
			
			return actIndex;
		}
		
		/**
		  Writes the header of the document
		  @param movieTitle String with the title of the movie
		  @param delay Number with the number of seconds of delay of each scene
		  @param description String with the description of the movie
		  @param authors Array of contributors as Strings
		  @return String formated as XML
		*/
		static public function writeHeader(movieTitle:String, delay:Number, description:String, authors:Array):String{
			var str:String = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><animation>"+
							"<header><title>"+movieTitle+"</title>"+
							"<delay>"+String(delay)+"</delay>"+
							"<description>"+description+"</description>"+
							"<version>"+Editor.VERSION+"</version>";
							
			for each(var author:String in authors){
				str += "<contributor>"+author+"</contributor>";
			}
			str += "</header>";
			return str;
		}
		
		
		/**
		  Writes the body of the document
		  @param containers Vector of scenes of the movie
		  @return String formated as XML
		*/
		static public function writeBody(containers:Vector.<Container>):String{
			var str:String = "<body>";
			for each (var scene:Container in containers){
				str += FileModule.writeActions(scene.getActions(), scene.getType());
			}
			return str + "</body>";
		}
		
		/**
		  Writes the end and foot of the document
		  @return String formated as XML
		*/
		static public function writeDocumentFoot():String{
			return "</animation><!-- Created with OnlineMovieEditor in PLP. By Sergio Rodriguez Lumley and Daniel Guerrero Martinez-->";
		}
		
		
		/************ PRIVATE FUNCTIONS ***********/
		
		/**
		  Returns a String formated in the style of XML with a representation of this container's actions
		  @return String with the representation of the actions
		*/
		static private function writeActions(actions:ActionList, sceneType:uint):String{
			var str:String;
			var actIndex:Array = new Array(11); // This is a multidimensional array, one dimension for each kind of action
			var irrelevantShapes:Array = new Array(); // Actions that have been created and removed in the same scene
			var i:int=0;
			var j:int=0;
			var indtmp:Array;
			var found:Boolean = false;
			var moved:Boolean = false;
			
			// What will each index of actIndex allocate?
			var INS:		uint = 0; // 0 - insert
			var DEPTH:		uint = 1; // 1 - change_depth
			var REMV:		uint = 2; // 2 - remove
			var MOV:		uint = 3; // 3 - move
			var SCALE:		uint = 4; // 4 - scale
			var ROT:		uint = 5; // 5 - rotate
			var TXT:		uint = 6; // 6 - change_text
			var BLUR:		uint = 7; // 7 - blur
			var PAINT:		uint = 8; // 8 - paint
			// Always insert here new actions, displace actions below this one
			var INVREMV:	uint = 9; // 9 - invalidate_remove
			var INSDEPTH:	uint = 10; // 10 - insert+change_depth in creation order (leave this one always the last one)
			for(i = 0; i<11; ++i) actIndex[i] = new Array();
			
			// Fill actions
			for(i = 0; i<actions.size(); ++i){
				switch(actions.get(i).getType()){
					case Action.INSERT:
						actIndex[INS].push(i);
						break;
					case Action.REMOVE:
						// Look for insert actions in this scene with the same
						// ShapeID; if found, this series of actions are
						// irrelevant and will be removed after
						found = false;
						for(j=0; j<actIndex[INS].length && !found; ++j){
							if(actions.get(i).getShapeID() == actions.get(actIndex[INS][j]).getShapeID()){
								found = true;
								irrelevantShapes.push(actions.get(i).getShapeID());
								//trace("found irrelevant action with "+actions.get(i).getShapeID());
							}
						}
						if(! found)	actIndex[REMV].push(i);
						break;
					case Action.MOVE:
						actIndex[MOV].push(i);
						break;
					case Action.SCALE:
						actIndex[SCALE].push(i);
						break;
					case Action.ROTATE:
						actIndex[ROT].push(i);
						break;
					case Action.CHANGE_DEPTH:
						actIndex[DEPTH].push(i);
						break;
					case Action.CHANGE_TEXT:
						actIndex[TXT].push(i);
						break;
					case Action.BLUR:
						actIndex[BLUR].push(i);
						break;
					case Action.PAINT:
						actIndex[PAINT].push(i);
					case Action.INVALIDATE_REMOVE:
						// We look if this object was created in the same
						// stage, then we remove it from the irrelevant actions,
						// as it is relevant again (Although this should be impossible
						// with current build, future is never known!)
						found = false;
						for(j=0; j<irrelevantShapes.length && !found; ++j){
							if(irrelevantShapes[j] == actions.get(actIndex[INS][j]).getShapeID()){
								found = true;
								irrelevantShapes.splice(j, 1);
								//trace("found action "+actions.get(i).getShapeID()+" relevant again");
							}
						}
						actIndex[INVREMV].push(i);
					default:
						break;
				}
			}
			
			// Remove irrelevant actions from
			// insert/change_depth/remove/move/scale/rotate/change_text/blur/paint/invalidate_remove
			for(i=0; i<INSDEPTH; ++i){
				removeIrrelevantActionFrom(actIndex[i], irrelevantShapes, actions);
			}
			
			// Now that we have our indexes tidy and clean, we search which of the actions
			// from remove and invalidate_remove were last. If invalidate_remove was last,
			// then we remove "remove" action from the list, else we leave everything as it is
			for(i=0; i< actIndex[REMV].length; ++i){
				found = false;
				for(j=0; j<actIndex[INVREMV].length && !found; ++j){
					if(actions.get(actIndex[REMV][i]).getShapeID() ==
						actions.get(actIndex[INVREMV][j]).getShapeID()){
						found = true;
						// If invalidate_remove happened after this remove
						if(actIndex[INVREMV][j] > actIndex[REMV][i]){
							actIndex[REMV].splice(i, 1); // remove this "remove"
							// so now we have one less position in actIndex[2],
							// that's why we have to subtract 1 to 'i'
							--i;
						}
					}
				}
			}
			
			// Merge in order all change_depth with inserts
			i=0; j=0;
			while(i<actIndex[INS].length || j<actIndex[DEPTH].length){
				if(j>= actIndex[DEPTH].length || actIndex[INS][i] < actIndex[DEPTH][j]){
					actIndex[INSDEPTH].push(actIndex[INS][i]);
					++i;
				}else{
					actIndex[INSDEPTH].push(actIndex[DEPTH][j]);
					++j;
				}
			}
			//for(i=0; i<actIndex.length; ++i)
				//trace(actIndex[i].toString());
			
			//********* TIME TO PRINT TO XML *************//
			str = "<scene><type>";
			switch(sceneType){
				case Container.NORMAL:
					str += "normal";
					break;
				case Container.INSTANT:
					str += "instant";
					break;
				case Container.READER:
					str += "reader";
					break;
				
			}
			str += "</type><actions>";
			
			// Print all change_depth and inserts in order...
			// ...compacting as much as possible in inserts
			while(actIndex[INSDEPTH].length > 0){
				// If it is a change_depth
				if(actions.get(actIndex[INSDEPTH][0]).getType() == Action.CHANGE_DEPTH){
					
					str += "<action><type>change_depth</type><id>"+
						actions.get(actIndex[INSDEPTH][0]).getShapeID()+
						"</id><num>"+actions.get(actIndex[INSDEPTH][0]).getNumber(1)+
						"</num></action>";
					actIndex[INSDEPTH].splice(0, 1);
					
				}else{ // Then it must be an INSERT one
				
					str += "<action><type>insert</type><id>"+
						actions.get(actIndex[INSDEPTH][0]).getShapeID()+
						"</id><classtype>"+actions.get(actIndex[INSDEPTH][0]).getString(0)+
						"</classtype>";
					
					// Search for next change depth, as long as no inserts are found
					// only one change_depth can be found, as irrelevant actions have
					// been removed before
					found = false;
					for(i=1; i<actIndex[INSDEPTH].length && !found; ++i){
						if(actions.get(actIndex[INSDEPTH][i]).getType() == Action.INSERT){
							found = true; // Insert has been found, stop loop here
						}else if(actions.get(actIndex[INSDEPTH][i]).getShapeID() == 
								actions.get(actIndex[INSDEPTH][0]).getShapeID()){
							found = true;
							str += "<depth>"+ actions.get(actIndex[INSDEPTH][i]).getNumber(1) +"</depth>";
							actIndex[INSDEPTH].splice(i, 1);
						}
					}
					
					// Search of any other action to compact inside the insert
					moved = false;
					// [REMOVE, INVALIDATE_REMOVE), so here we ignore INSERTS, DEPTH and INVALIDATE_REMOVE
					// note that DEPTH is treated in the loop just above
					for(i=REMV; i<INVREMV; ++i){
						found = false;
						for(j=0; j<actIndex[i].length && !found; ++j){
							if(actions.get(actIndex[i][j]).getShapeID() ==
								actions.get(actIndex[INSDEPTH][0]).getShapeID()){
								found = true;
								switch(i){
									case REMV: // remove -- This case is impossible, just for debugging
										trace("FileModule: getActionsAsXML() THERE MUST BE A HUGE MISTAKE HERE");
										break;
									case MOV: // move
										moved = true;
										str += "<x>"+actions.get(actIndex[i][j]).getNumber(2)+
												"</x><y>"+actions.get(actIndex[i][j]).getNumber(3)+"</y>";
										break;
									case SCALE: // scale
										str += "<scalex>"+actions.get(actIndex[i][j]).getNumber(2)+
												"</scalex><scaley>"+actions.get(actIndex[i][j]).getNumber(3)+"</scaley>";
										break;
									case ROT: // rotate
										str += "<rotate>"+actions.get(actIndex[i][j]).getNumber(1)+"</rotate>";
										break;
									case TXT: // change_text
										str += "<text>"+actions.get(actIndex[i][j]).getString(1)+"</text>";
										break;
									case BLUR: // blur
										str += "<blurx>"+actions.get(actIndex[i][j]).getNumber(2)+
												"</blurx><blury>"+actions.get(actIndex[i][j]).getNumber(3)+"</blury>";
										break;
									case PAINT: // paint
										str += "<paint>"+FileModule.getPaintString(actions.get(actIndex[i][j]))+"</paint>";
										break;
								}
								
								actIndex[i].splice(j, 1);
							}
						}
					}
					
					// If it has not been moved, then we put the original coordinates
					if(! moved){
						str += "<x>"+actions.get(actIndex[INSDEPTH][0]).getNumber(0)+
								"</x><y>"+actions.get(actIndex[INSDEPTH][0]).getNumber(1)+"</y>";
					}
					actIndex[INSDEPTH].splice(0, 1);
					
					str += "</action>";
				} // End IF Action was an INSERT action
			} // Finished with change_depth / insert actions
			
			// Now we complete the rest of the actions, leaving removes to the end
			for(i=INVREMV-1; i>=REMV; --i){
				for(j=0; j<actIndex[i].length; ++j){
					str += "<action><type>";
					switch(i){
						case REMV: // remove
							str += "remove</type><id>"+actions.get(actIndex[i][j]).getShapeID()+"</id>";
							break;
						case MOV: // move
							str += "move</type><id>"+actions.get(actIndex[i][j]).getShapeID()+
									"</id><num>"+actions.get(actIndex[i][j]).getNumber(2)+
									"</num><num>"+actions.get(actIndex[i][j]).getNumber(3)+"</num>";
							break;
						case SCALE: // scale
							str += "scale</type><id>"+actions.get(actIndex[i][j]).getShapeID()+
									"</id><num>"+actions.get(actIndex[i][j]).getNumber(2)+
									"</num><num>"+actions.get(actIndex[i][j]).getNumber(3)+"</num>";
							break;
						case ROT: // rotate
							str += "rotate</type><id>"+actions.get(actIndex[i][j]).getShapeID()+
									"</id><num>"+actions.get(actIndex[i][j]).getNumber(1)+"</num>";
							break;
						case TXT: // change_text
							str += "change_text</type><id>"+actions.get(actIndex[i][j]).getShapeID()+
									"</id><text>"+actions.get(actIndex[i][j]).getString(1)+"</text>";
							break;
						case BLUR: // blur
							str += "blur</type><id>"+actions.get(actIndex[i][j]).getShapeID()+
									"</id><num>"+actions.get(actIndex[i][j]).getNumber(2)+
									"</num><num>"+actions.get(actIndex[i][j]).getNumber(3)+"</num>";
							break;
						case PAINT: // paint
							str += "paint</type><id>"+actions.get(actIndex[i][j]).getShapeID()+"</id>"+
								FileModule.getPaintString(actions.get(actIndex[i][j]));
							break;
					}
					str += "</action>";
					//actIndex[i].splice(j, 1); No need of this now, because we run it backwards
				}
			}
			
			str+="</actions></scene>";
			return str;
		}
		
		/**
		  Returns all the content of a PAINT action as a String written in XML format.
		  Why a separate function only for this? PAINT action is way too complex to
		  insert it directly in the code
		  @param act Action assumed to be a PAINT action, this is not checked here
		  @return String of the action's content in XML format
		*/
		static private function getPaintString(act:Action):String{
			var str:String = "";
			var bread:uint = 0; // Base Number of actions read
			var read:uint = 0; // Number of actions read (for points and lines)
			var i:uint = 0;
			var j:uint = 0;
			
			for (i = 0; i<act.getNumber(0); ++i){ // Number of pair of color-numlines
				str += "<color>"+String(act.getNumber(i*2+1))+"</color>"+
						"<coloredlines>"+String(act.getNumber(i*2+2))+"</coloredlines>";
			}
			// We situate bread in <lines>
			bread = i*2+1; // <ncolors>+ [<color><coloredlines>]*   = <nlines>
			read = bread+1;
			
			for (i = 0; i<act.getNumber(bread); ++i){ // For each line
				str += "<line>";
				for (j = 0; j<act.getNumber(read); ++j){ // For each point
					str += "<x>"+ act.getNumber(read+1+j*2) +"</x><y>"+ act.getNumber(read+2+j*2) +"</y>";
				}
				read += j*2+1; // When j > <npoints>, we have passed one iteration
				str += "</line>";
			}
			
			return str;
			
		}
		
		/**
		  Reads the XML object representing an action and returns
		  an action object from it
		*/
		static private function xmlInsertAction(act:XML, actions:ActionList):uint{
			var numactions:uint = 1;
			switch(Action.translateToCode(act.type)){
				case Action.INSERT:
					actions.push(new Action(Action.INSERT, act.id, act.classtype, act.x, act.y));
					if(act.child("text").length() > 0){
						actions.push(new Action(Action.CHANGE_TEXT, act.id, "-", act.text.text()));
						++numactions;
					}
					if(act.depth.toString().length > 0){
						actions.push(new Action(Action.CHANGE_DEPTH, act.id, 0, act.depth));
						++numactions;
					}
					if(act.scalex.toString().length >0 || act.scaley.toString().length >0){
						++numactions;
						if(act.scalex.toString().length > 0 && act.scaley.toString().length <= 0)
							actions.push(new Action(Action.SCALE, act.id, 1, 1, act.scalex, 1));
						else if(act.scalex.toString().length <= 0 && act.scaley.toString().length > 0)
							actions.push(new Action(Action.SCALE, act.id, 1, 1, 1, act.scaley));
						else 
							actions.push(new Action(Action.SCALE, act.id, 1, 1, act.scalex, act.scaley));
					}
					if(act.rotate.toString().length > 0){
						actions.push(new Action(Action.ROTATE, act.id, 0, act.rotate));
						++numactions;
					}
					if(act.blurx.toString().length >0 || act.blury.toString().length >0){
						++numactions;
						if(act.blurx.toString().length > 0 && act.blury.toString().length <= 0)
							actions.push(new Action(Action.BLUR, act.id, 0, 0, act.blurx, 1));
						else if(act.blurx.toString().length <= 0 && act.blury.toString().length > 0)
							actions.push(new Action(Action.BLUR, act.id, 0, 0, 1, act.blury));
						else 
							actions.push(new Action(Action.BLUR, act.id, 0, 0, act.blurx, act.blury));
					}
					if(act.paint.toString().length > 0){
						++numactions;
						actions.push(new Action(Action.PAINT, act.id, FileModule.getPaintArray(act.paint[0])));
						// We use act.paint[0] because we know for certain that there will only be one
						// paint action, as this action is unique
					}
					break;
				case Action.REMOVE:
					actions.push(new Action(Action.REMOVE, act.id));
					break;
				case Action.MOVE:
					actions.push(new Action(Action.MOVE, act.id, 0, 0, act.num[0], act.num[1]));
					break;
				case Action.SCALE:
					actions.push(new Action(Action.SCALE, act.id, 1, 1, act.num[0], act.num[1]));
					break;
				case Action.ROTATE:
					actions.push(new Action(Action.ROTATE, act.id, 0, act.num));
					break;
				case Action.CHANGE_DEPTH:
					actions.push(new Action(Action.CHANGE_DEPTH, act.id, 0, act.num));
					break;
				case Action.CHANGE_TEXT:
					actions.push(new Action(Action.CHANGE_TEXT, act.id, "-", act.text));
					break;
				case Action.BLUR:
					actions.push(new Action(Action.BLUR, act.id, 0, 0, act.num[0], act.num[1]));
				case Action.PAINT:
					actions.push(new Action(Action.PAINT, act.id, FileModule.getPaintArray(act)));
				default:
					// Ignore invalids or bad formed
					//actions.push(new Action(Action.INVALID, ""));
					numactions = 0;
					break;
			}
			return numactions;
			
		}
		
		/**
		  Returns all the content of a PAINT action as an Array.
		  Why a separate function only for this? PAINT action is way too complex to
		  insert it directly in the code
		  @param act XML action assumed to be a PAINT action, this is not checked here
		  @return Array with all the contents of the action, just as specified in Action file
		*/
		static private function getPaintArray(act:XML):Array{
			var array:Array = new Array(); // We will use push
			var i:uint 		= 0;
			var j:uint		= 0;
			
			array.push(Number(act.color.length())); // Number of pair of color-coloredlines
			for(i = 0; i< act.color.length(); ++i){
				array.push(Number(act.color[i]));
				array.push(Number(act.coloredlines[i]));
			}
			
			array.push(Number( act.line.length() )); // Number of lines
			for(i = 0; i< act.line.length(); ++i){
				array.push( Number (act.line[i].x.length())); // Number of points (each point is X and Y)
				for(j = 0; j< act.line[i].x.length(); ++j){
					array.push(Number(act.line[i].x[j]));
					array.push(Number(act.line[i].y[j]));
				}
			}
			
			return array;
			
		}
		
		/**
		  Removes irrelevant actions from the index of actions, used in XML writing
		  @param actIndex Array with the a specified kind of actions (all the insert actions, or all the move actions...)
		  @param irrelevantShapes Array with all shapes that have been created and removed from this scene (so all of their actions are irrelevant)
		  @param actions ActionList of a scene
		*/
		static private function removeIrrelevantActionFrom(actIndex:Array, irrelevantShapes:Array, actions:ActionList):void{
			var indtmp:Array = new Array();
			for(var i:int=actIndex.length-1; i>= 0; --i){
				// If it's not the last action or it hasn't been marked as irrelevant, include
				if(indtmp.indexOf(actions.get(actIndex[i]).getShapeID()) < 0 &&
					irrelevantShapes.indexOf(actions.get(actIndex[i]).getShapeID()) < 0 ){
					indtmp.push(actions.get(actIndex[i]).getShapeID());
				}else{
					//trace("removing: "+ actions.get(actIndex[i]).printString());
					actIndex.splice(i, 1);
				}
			}
		}
		
	}
}
